/**
 * 210910 created by jordia
 * 211026: 扩充补面，更改部分对外方法
 * 211029: 修改updateTargets为直接变换geometry，适应非均匀的缩放
 * 20220304:[require] to [import]...jordia
 */
import {NotEqualStencilFunc,MeshStandardMaterial,Color,Matrix4,ReplaceStencilOp,Object3D,Float32BufferAttribute,Mesh,
    Line,LineBasicMaterial,BufferGeometry,BufferAttribute,MeshBasicMaterial,AlwaysStencilFunc,BackSide,IncrementWrapStencilOp,
    FrontSide,DecrementWrapStencilOp} from 'three';

const MAX_COUNT=32;
let BACK_STENCIL_MATERIAL= new MeshBasicMaterial({
    depthWrite: false,
    colorWrite: false,
    stencilWrite: true,
    stencilFunc: AlwaysStencilFunc,
    side : BackSide,
    stencilFail : IncrementWrapStencilOp,
    stencilZFail : IncrementWrapStencilOp,
    stencilZPass : IncrementWrapStencilOp,
});
let FRONT_STENCIL_MATERIAL=new MeshBasicMaterial({
    depthWrite: false,
    colorWrite: false,
    stencilWrite: true,
    stencilFunc: AlwaysStencilFunc,
    side : FrontSide,
    stencilFail : DecrementWrapStencilOp,
    stencilZFail : DecrementWrapStencilOp,
    stencilZPass : DecrementWrapStencilOp,
});
const NULL_FUNCTION=()=>{};

/**
 * 任意平面裁切（补面）
 * 注意：裁切之前必须设置完所有参数，要修改参数必须先关闭裁切
 *       不要修改此对象实例的缩放。
 * @param renderer {WebGLRenderer} 渲染器
 * @param options {Object} 可选参数对象：
 *      debugSize:调试图形大小
 *      debugColor:调试图形颜色
 *      size:裁切面大小（应设置为能完全覆盖所有目标）
 *      cutMaterial:裁切面材质（补面）
 *
 * 示例代码：
 *      renderer.localClippingEnabled=true;
 *      let clip=new PlanesClipTargets(renderer,{
 *              debugSize:3,
 *              debugColor:new Color('#ffff00'),
 *              size:5
 *      });
 *      clip.setPlanes(
 *          new Plane(new Vector3(0,1,0),1),
 *          new Plane(new Vector3(0,-1,0),1)
 *      );
 *      clip.debug=true;
 *      scene.add(clip);
 *
 *      clip.addTargets(...tars);
 *      clip.begin=true;
 *
 *      setInterval(()=>{
 *              for(let i=0;i<tars.length;i++){
 *                  tars[i].rotation.x+=0.01;
 *                  tars[i].position.x+=0.01;
 *              }
 *              clip.updateTargets();
 *
 *              clip.rotation.x+=0.01;
 *              clip.position.x+=0.01;
 *              clip.updateMatrixWorld();
 *              clip.updatePlanes();
 *
 *      },50);
 */
export default class PlanesClipTargets extends Object3D{
    constructor(renderer=null,options={}){
        super();
        this._renderer=renderer;
        this.init(options);
    }

    //......................................public.........................................

    /**
     * 裁切面（补面）材质
     * @param mat
     */
    set cutMaterial(mat){
        if(this._began){
            console.warn('set [.begin=false] to change default cut-material!');
            return;
        }
        this._cutMaterial=mat.clone();
    }

    /**
     * 开启/关闭调试
     * @param bol
     */
    set debug(bol){
        if(bol){
            this.debug=false;
            for(let p of this._lockPlanes){
                let s=this._debugSize*0.5;
                let g1=new BufferGeometry();
                g1.setAttribute('position',new BufferAttribute(new Float32Array([
                    -s,s,0, -s,-s,0, s,-s,0, s,s,0, -s,s,0, s,-s,0, s,s,0, -s,-s,0, 0,0,0, 0,0,s
                ]),3));
                g1.lookAt(p.normal);
                g1.translate(-p.normal.x*p.constant,-p.normal.y*p.constant,-p.normal.z*p.constant);
                let h=new Line(g1,new LineBasicMaterial({
                    color:this._debugColor
                }));
                this._debug.push(h);
            }
            this.add(...this._debug);
        }else{
            for(let d of this._debug){
                this.remove(d);
                d.material.dispose();
                d.geometry.dispose();
            }
            this._debug.length=0;
        }
    }

    /**
     * 开始/结束裁切
     * @param bol
     */
    set begin(bol){
        if (!this._targets.length) {
            console.warn('targets needs to be set up first!');
            return;
        }
        if(bol){
            if(this._began)return;
            if (!this._renderer.localClippingEnabled) {
                console.warn('Property [localClippingEnabled] of renderer is disabled, please enable this property!');
                return;
            }
            if(this._planes.length===0){
                console.warn('no data for clipping, use [addPlanes] to add some data!');
                return;
            }
            for(let i=0;i<this._targets.length;i++){
                let tar=this._targets[i];
                if(!tar.material)continue;
                this._oriMaterials[tar.uuid+'']=tar.material.clone();
                tar.material.clippingPlanes=this._planes;
            }
            this.setStencil();
            this.syncBoardWithPlane();

        }else{
            if(!this._began)return;
            for(let i=0;i<this._targets.length;i++){
                let tar=this._targets[i];
                tar.material.dispose();
                tar.material=this._oriMaterials[tar.uuid+''].clone();
                this._oriMaterials[tar.uuid+''].dispose();
            }
            for(let board of this._boards){
                this.remove(board);
                board.material.distanceTo();
                board.geometry.dispose();
            }
            for(let gp of this._stencilGroups){
                this.remove(gp);
                for(let c of gp.children){
                    c.material.dispose();
                }
            }
            this._boards.length=0;
            this._stencilGroups.length=0;
            this._transforms={};
        }
        this._began=bol;
    }

    /**
     * 添加裁切对象
     * @param targets
     */
    addTargets(...targets){
        if(this._began){
            console.warn('set [.begin=false] to add new targets!');
            return;
        }
        for(let tar of targets){
            let idx=this._targets.findIndex((t)=>t.uuid===tar.uuid);
            if(idx<0){
                this._targets.push(tar);
            }
        }
    }

    /**
     * 移除裁切对象
     * @param targets
     */
    removeTargets(...targets){
        if(this._began){
            console.warn('set [.begin=false] to remove targets!');
            return;
        }
        for(let tar of targets){
            let idx=this._targets.findIndex((t)=>t.uuid===tar.uuid);
            if(idx<0)continue;
            this._targets.splice(idx,1);
        }
    }

    /**
     * 清除裁切对象
     */
    clearTargets(){
        this.removeTargets(...this._targets);
    }

    /**
     * 设置裁切平面
     * @param planes
     */
    setPlanes(...planes){
        if(this._began){
            console.warn('set [.begin=false] to add new planes!');
            return;
        }
        for(let pn of planes){
            if(this._planes.length>MAX_COUNT){
                console.warn('[PlanesClipTargets]: Too many clipping planes [maximum 32] can result in poor performance!');
                return;
            }
            let idx=this._planes.findIndex((p)=>p.equals(pn));
            if(idx<0){
                this._planes.push(pn.clone());
                this._lockPlanes.push(pn.clone());
            }
        }
    }

    /**
     * 清除裁切平面
     */
    clearPlanes(){
        if(this._began){
            console.warn('set [.begin=false] to clear planes!');
            return;
        }
        this._planes.length=this._lockPlanes.length=0;
    }

    /**
     * 由变换目标引起的更新
     */
    updateTargets(){
        for(let k in this._transforms){
            let tf=this._transforms[k+''];
            let src=tf.src;
            this._inv.copy(tf.matrix).invert();
            for(let t of tf.dst){
                t.geometry.applyMatrix4(this._inv);
                t.geometry.applyMatrix4(src.matrixWorld);
            }
            tf.matrix.copy(src.matrixWorld);
        }
    }

    /**
     * 由变换组件本身引起的更新
     */
    updatePlanes(){
        for(let i=0;i<this._lockPlanes.length;i++){
            this._planes[i].copy(this._lockPlanes[i]).applyMatrix4(this.matrixWorld);
        }
        for(let gp of this._stencilGroups){
            this._inv.copy(gp.matrixWorld).invert();
            gp.applyMatrix4(this._inv);
        }
    }

    //......................................private.........................................

    syncBoardWithPlane(){
        for(let i=0;i<this._planes.length;i++){
            this._boards[i].scale.set(this._size,this._size,this._size);
            this._boards[i].position.copy(this._planes[i].normal).multiplyScalar(this._planes[i].constant).negate();
            this._boards[i].lookAt(0,0,0);
        }
    }
    createBoard(i,material) {
        let g = new BufferGeometry();
        let ps = new Float32Array([-1, 1, 0,  -1, -1, 0,  1, -1, 0,  1, 1, 0]);
        let uv = new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]);
        g.setIndex([0, 2, 1, 0, 3, 2]);
        g.setAttribute('position', new Float32BufferAttribute(ps, 3));
        g.setAttribute('uv', new Float32BufferAttribute(uv, 2));
        g.computeVertexNormals();
        g.computeBoundingSphere();
        g.setDrawRange(0, Number.POSITIVE_INFINITY);
        let board = new Mesh(g, material);
        board.raycast=NULL_FUNCTION;
        board.onAfterRender=()=>{
            this._renderer.clearStencil();
        };
        this._boards[i]=board;
    }
    setStencil(){
        let mat=this._cutMaterial.clone();
        mat.stencilWrite= true;
        mat.stencilRef= 0;
        mat.stencilFunc= NotEqualStencilFunc;
        mat.stencilFail= ReplaceStencilOp;
        mat.stencilZFail= ReplaceStencilOp;
        mat.stencilZPass= ReplaceStencilOp;
        let order=this._renderOrder;
        let uOrder=0.1/(this._planes.length+1);
        for(let i=0;i<this._planes.length;i++){
            order+=uOrder;
            let bMat=mat.clone();
            bMat.clippingPlanes=this._planes.filter(p=>p!==this._planes[i]);
            this.createBoard(i,bMat);
            this.createPlaneStencilGroup(i,order);
            this._boards[i].renderOrder = order+uOrder*0.1;
            this.add(this._boards[i]);
            this.add(this._stencilGroups[i]);
        }
        mat.dispose();
    }
    createPlaneStencilGroup(k,order){
        let gp=this._stencilGroups[k]=new Object3D();
        for(let i=0;i<this._targets.length;i++){
            let tar=this._targets[i];
            tar.updateMatrixWorld();
            if(!tar.material)continue;
            if(!this._transforms[tar.uuid+'']){
                this._transforms[tar.uuid+'']={
                    src:tar,
                    dst:[],
                    matrix:new Matrix4().copy(tar.matrixWorld)
                };
            }
            let g=tar.geometry.clone().applyMatrix4(this._transforms[tar.uuid+''].matrix);
            // back faces
            let matBack=BACK_STENCIL_MATERIAL.clone();
            matBack.clippingPlanes = [this._planes[k]];
            let meshBack = new Mesh(g,matBack);
            meshBack.raycast=NULL_FUNCTION;
            meshBack.renderOrder=order;
            gp.add( meshBack );
            // front faces
            let matFront=FRONT_STENCIL_MATERIAL.clone();
            matFront.clippingPlanes=[this._planes[k]];
            let meshFront=new Mesh(g.clone(),matFront);
            meshFront.raycast=NULL_FUNCTION;
            meshFront.renderOrder = order;
            gp.add( meshFront );
            this._transforms[tar.uuid+''].dst.push(meshBack,meshFront);
        }
    }

    init(options){
        this.initVariable(options);
    }
    initVariable(options){
        this._targets=[];
        this._planes=[];
        this._lockPlanes=[];
        this._oriMaterials={};
        this._debug=[];
        this._cutMaterial=options['cutMaterial']||new MeshStandardMaterial({color:new Color('#ff0300')});
        this._renderOrder=1;
        this._boards=[];
        this._stencilGroups=[];
        this._transforms={};
        this._size=options['size']||4;
        this._inv=new Matrix4();
        this._debugSize=options['debugSize']||1;
        this._debugColor=options['debugColor']||new Color('#ff00ff');
    }
}